/*
 * Copyright (c) 2024 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {
  ClassDeclaration,
  Decorator,
  Expression,
  MethodDeclaration,
  PropertyAccessExpression,
  PropertyAssignment,
  SourceFile,
  SyntaxKind
} from 'ts-morph';
import { AnalyzerResultLike, HMServiceResult } from './common/PluginModel';
import { HMRouterPluginConfig } from './HMRouterPluginConfig';
import { Logger, PluginError } from './common/Logger';
import { TsAstUtil } from './utils/TsAstUtil';
import HMRouterPluginConstant from './constants/CommonConstants';
import HMFileUtil from './utils/FileUtil';
import ConfigConstants from './constants/ConfigConstants';
import PluginStore from './store/PluginStore';

export class AnalyzerController {
  private analyzeResult: Set<AnalyzerResultLike> = new Set();

  /**
   * @description analyze file
   * @param sourceFilePath
   * @param config
   */
  analyzeFile(sourceFilePath: string, config: HMRouterPluginConfig) {
    let analyzerService = new AnalyzerService(sourceFilePath, config);
    analyzerService.start();
    analyzerService.getResult().forEach(item => {
      item.pageSourceFile = sourceFilePath;
      this.analyzeResult.add(item);
    });
    this.parseConstants();
  }

  parseConstants() {
    this.analyzeResult.forEach((item) => {
      Object.getOwnPropertyNames(item).forEach((key: any) => {
        let propertyValue = Reflect.get(item, key);
        propertyValue = this.parsePropertyValue(propertyValue);
        if (propertyValue === '') {
          Logger.error(PluginError.ERR_NOT_EMPTY_STRING);
          throw new Error('constants value cannot be an empty string, filePath:' + item.pageSourceFile);
        }
        Reflect.set(item, key, propertyValue);
      });
    });
  }

  private parsePropertyValue(propertyValue: any): any {
    if (propertyValue.type === 'constant') {
      // Constant type value
      try {
        return TsAstUtil.parseConstantValue(TsAstUtil.getSourceFile(propertyValue.variableFilePath),
          propertyValue.variableName);
      } catch (error: any) {
        Logger.error(`Failed to parse constant ${propertyValue.variableName}: ${error.message}`);
        throw new Error(`Failed to parse constant ${propertyValue.variableName}: ${error.message}`);
      }
    } else if (propertyValue.type === 'object') {
      // Static variable type value
      try {
        return TsAstUtil.parseConstantValue(TsAstUtil.getSourceFile(propertyValue.variableFilePath),
          propertyValue.variableName, propertyValue.propertyName);
      } catch (error: any) {
        Logger.error(`Failed to parse object property ${propertyValue.variableName}.${propertyValue.propertyName}: ${error.message}`);
        throw new Error(`Failed to parse object property ${propertyValue.variableName}.${propertyValue.propertyName}: ${error.message}`);
      }
    } else if (propertyValue.type === 'array') {
      // Array type value
      return propertyValue.value.map((item: any) => {
        return this.parsePropertyValue(item);
      });
    } else {
      // Ordinary type value
      return propertyValue;
    }
  }

  /**
   * @description Obtain the analysis result set
   */
  getAnalyzeResultSet() {
    return this.analyzeResult;
  }

  clearAnalyzeResultSet() {
    this.analyzeResult.clear();
  }
}

export class AnalyzerService {
  private readonly sourceFilePath: string;
  private sourceFile: SourceFile;
  private config: HMRouterPluginConfig;
  private analyzerResultSet: Set<AnalyzerResultLike> = new Set();
  private importMap: Map<string, string[]> = new Map();

  constructor(sourceFilePath: string, config: HMRouterPluginConfig) {
    this.sourceFilePath = sourceFilePath;
    this.sourceFile = TsAstUtil.getSourceFile(sourceFilePath);
    this.config = config;
  }

  // startup analyzer
  start() {
    // Parse the imported variables
    this.analyzeImport();
    // Parse the HMRouter tag
    this.analyzeRouter();
    // Parse the lifecycle, animation, interceptor, and service route tags
    this.analyzeComponent();
    // Parse the file line by line to determine if it contains NavDestination
    this.parseFileByLineOrder();
  }

  /**
   * @description Get the analysis result set
   */
  getResult() {
    // Check if there are multiple HMRouter annotations
    let HMRouterNum = 0;
    this.analyzerResultSet.forEach((analyzerResult: AnalyzerResultLike) => {
      if (analyzerResult.annotation === HMRouterPluginConstant.ROUTER_ANNOTATION) {
        HMRouterNum++;
      }
    });
    if (HMRouterNum > 1) {
      Logger.error(PluginError.ERR_REPEAT_ANNOTATION, this.sourceFilePath);
      throw new Error(`File:${this.sourceFilePath} exists more than one @HMRouter annotation`);
    }
    return this.analyzerResultSet;
  }

  /**
   * @description Parse the Import type node
   * @private
   */
  private analyzeImport() {
    this.sourceFile.getImportDeclarations().forEach((importDeclaration) => {
      const moduleSpecifier = importDeclaration.getModuleSpecifierValue();
      const namedImports = importDeclaration.getNamedImports().map((namedImport) => namedImport.getName());
      const defaultImport = importDeclaration.getDefaultImport()?.getText();
      const namespaceImport = importDeclaration.getNamespaceImport()?.getText();
      const importNames: string[] = [];

      if (namedImports.length > 0) {
        importNames.push(...namedImports);
      }
      if (defaultImport) {
        importNames.push(defaultImport);
      }
      if (namespaceImport) {
        importNames.push(namespaceImport);
      }

      if (importNames.length > 0) {
        // 检查是否已存在该模块的导入
        if (this.importMap.has(moduleSpecifier)) {
          // 合并导入名称而不是覆盖
          const existingImports = this.importMap.get(moduleSpecifier)!;
          this.importMap.set(moduleSpecifier, [...new Set([...existingImports, ...importNames])]);
        } else {
          this.importMap.set(moduleSpecifier, importNames);
        }
      }
    });
  }

  /**
   * @description analyze Router
   * @private
   */
  private analyzeRouter() {
    // Get all ExpressionStatement in the file to obtain the component name following the struct keyword
    let viewNameArr = this.sourceFile
      .getChildrenOfKind(SyntaxKind.ExpressionStatement)
      .map((node) => {
        return node.getText();
      })
      .filter((text) => {
        return text != 'struct';
      });

    // Get the HMRouter tag in the file where the declaration is missing and parse the parameters
    this.sourceFile.getChildrenOfKind(SyntaxKind.MissingDeclaration).forEach((node, index) => {
      // Parse the decorator where the declaration is missing
      node.getChildrenOfKind(SyntaxKind.Decorator).forEach((decorator) => {
        this.addToResultSet(decorator, viewNameArr[index]);
      });
    });

    this.sourceFile.getExportAssignments().forEach((exportAssignment, index) => {
      exportAssignment.getDescendantsOfKind(SyntaxKind.Decorator).forEach((decorator) => {
        let result = this.addToResultSet(decorator, viewNameArr[index]);
        result.isDefaultExport = true;
      });
    });
  }

  /**
   * @description Parse the file line by line to determine if it contains NavDestination
   * @private
   */
  private parseFileByLineOrder() {
    // 1. Get all top-level statements
    const statements = this.sourceFile.getStatements();
    // 2. Sort them by their position (start position) in the code
    const sortedStatements = statements.sort((a, b) => a.getStart() - b.getStart());
    let HMRouterExists: boolean = false;
    // 存储是否开启custom模式
    let useNavDst: boolean = false;

    // 3. Parse line by line
    sortedStatements.forEach((statement) => {
      // Identify missing declarations and check if they contain HMRouter
      if (statement.getKind() === SyntaxKind.MissingDeclaration && statement.getText().includes('HMRouter')) {
        // 重置标志位
        HMRouterExists = true;
        useNavDst = false;

        // 检查@HMRouter注解是否设置了useNavDst=true
        const text = statement.getText();
        // 匹配是否包含useNavDst: true 或 useNavDst:true 格式的参数
        if (/(useNavDst\s*:\s*true)/.test(text)) {
          useNavDst = true;
          Logger.info('Found HMRouter with useNavDst=true, skipping NavDestination check');
        }
      }

      //  When parsing line by line, the code block immediately follows HMRouter
      if (statement.getKind() === SyntaxKind.Block && HMRouterExists) {
        // Determine the completion of the reset state
        HMRouterExists = false;

        // 当custom为true时，跳过NavDestination检查
        if (useNavDst) {
          return;
        }

        let reg = new RegExp(/NavDestination\s*\(\s*\)/);
        let text = statement.getText();
        // Remove strings and comments to ensure only expressions are matched
        const cleanedCodeBlock = text
          .replace(/(["'`]).*?\1/g, '') // Remove strings
          .replace(/\/\/.*|\/\*[\s\S]*?\*\//g, ''); // Remove single-line and multi-line comments
        if (reg.test(cleanedCodeBlock)) {
          Logger.error(PluginError.ERR_WRONG_DECORATION);
          throw new Error('NavDestination is not allowed in HMRouter, filePath:' + this.sourceFilePath);
        }
      }
    });
  }

  /**
   * @description Parse the component tags @HMService @HMAnimation @HMInterceptor @HMLifecycle
   * @private
   */
  private analyzeComponent() {
    // Get all classes within the file
    this.sourceFile.getClasses().forEach((cls: ClassDeclaration) => {
      //  Get all decorators of the class and parse them
      cls.getDecorators().forEach((decorator: Decorator) => {
        if (this.config.annotation.includes(decorator.getName())) {
          this.addToResultSet(decorator, cls.getName()!);
        }
      });
      // Get all methods within the class and parse the HMService tags of the methods
      cls.getMethods().forEach((method: MethodDeclaration) => {
        method.getDecorators().forEach((decorator: Decorator) => {
          let serviceResult = this.addToResultSet(decorator, cls.getName()!) as HMServiceResult;
          serviceResult.functionName = method.getName();
        });
      });
    });
  }

  /**
   * @description Add to the parse results
   * @param decorator The parsed decorator
   * @param componentName The parsed component name
   * @return AnalyzerResultLike:Parsed parameter object
   * @private
   */
  private addToResultSet(decorator: Decorator, componentName: string) {
    let decoratorResult = this.parseDecorator(decorator);
    decoratorResult.name = componentName;
    // Add to the parse results
    if (decoratorResult.annotation) {
      this.analyzerResultSet.add(decoratorResult);
    }
    return decoratorResult;
  }

  /**
   * @description parse decorator
   * @param decorator The parsed decorator
   * @private
   */
  private parseDecorator(decorator: Decorator): AnalyzerResultLike {
    let decoratorResult: AnalyzerResultLike = {};
    let decoratorName = decorator.getName();
    // Whether it is a tag provided by the framework
    if (this.config.annotation.includes(decoratorName)) {
      decoratorResult.annotation = decoratorName;
      let args: AnalyzerResultLike = this.parseDecoratorArguments(decorator);
      // Object Assignment
      Object.assign(decoratorResult, args);
    }
    return decoratorResult;
  }

  /**
   * @description parse decorator arguments
   * @param decorator The parsed decorator
   * @return AnalyzerResultLike:Parsed parameter object
   * @private
   */
  private parseDecoratorArguments(decorator: Decorator): AnalyzerResultLike {
    let argResult: AnalyzerResultLike = {};
    decorator.getArguments().map((arg) => {
      const objLiteral = arg.asKind(SyntaxKind.ObjectLiteralExpression);
      if (objLiteral) {
        objLiteral.getProperties().forEach((prop) => {
          let propertyName = (prop as PropertyAssignment).getName();
          let propertyValue = this.parseIdentifierPropertyValue((prop as PropertyAssignment).getInitializer()!);
          Reflect.set(argResult, propertyName, propertyValue);
        });
      }
    });
    return argResult;
  }

  /**
   * @description parse the value of pageURL
   * @param value The initial value of pageURL
   * @return any Parsed pageURL object
   * @private
   */
  private parseIdentifierPropertyValue(value: Expression): any {
    switch (value.getKind()) {
      case SyntaxKind.Identifier:
        if (value.getText() === 'undefined') {
          throw new Error(`Invalid property value, in ${this.sourceFilePath}`);
        }
        // constant value
        return {
          type: 'constant',
          variableName: value.getText(),
          variableFilePath: this.getVariableFilePath(value.getText()),
        };
      case SyntaxKind.PropertyAccessExpression:
        // Static variable value
        return {
          type: 'object',
          variableName: (value as PropertyAccessExpression).getExpression().getText(),
          propertyName: (value as PropertyAccessExpression)?.getName(),
          variableFilePath: this.getVariableFilePath((value as PropertyAccessExpression)?.getExpression().getText()),
        };
      case SyntaxKind.ArrayLiteralExpression:
        // Array value
        return {
          type: 'array',
          value: value
            .asKind(SyntaxKind.ArrayLiteralExpression)
          ?.getElements()
            .map((item) => this.parseIdentifierPropertyValue(item)),
        };
      default:
      // Fixed: Use correct method to parse property values
        return this.parsePrimitiveValue(value);
    }
  }

  /**
   * @description Parse primitive property values
   * @param value Expression to parse
   * @return Parsed value
   * @private
   */
  private parsePrimitiveValue(value: Expression): any {
    let propertyValue;
    switch (value.getKind()) {
      case SyntaxKind.StringLiteral:
        propertyValue = value.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
        break;
      case SyntaxKind.NumericLiteral:
        propertyValue = value.asKind(SyntaxKind.NumericLiteral)?.getLiteralValue();
        break;
      case SyntaxKind.TrueKeyword:
        propertyValue = true;
        break;
      case SyntaxKind.FalseKeyword:
        propertyValue = false;
        break;
      case SyntaxKind.ArrayLiteralExpression:
        propertyValue = value
          .asKind(SyntaxKind.ArrayLiteralExpression)
        ?.getElements()
          .map((item) => item.asKind(SyntaxKind.StringLiteral)?.getLiteralValue());
        break;
    }
    return propertyValue;
  }

  /**
   * @description get variable file Path
   * @param variableName
   * @return string, Variable file path
   * @private
   */
  private getVariableFilePath(variableName: string): string {
    let filePath: string = '';
    // 检查本地变量和类
    let classesNames = this.sourceFile.getClasses().map((classes) => {
      return classes.getName()!;
    });
    let variableNames = this.sourceFile.getVariableDeclarations().map((variableDeclaration) => {
      return variableDeclaration.getName();
    });
    if (classesNames.includes(variableName) || variableNames.includes(variableName)) {
      return this.sourceFilePath;
    }

    // Search in import map
    for (const [importPath, importNames] of this.importMap.entries()) {
      if (importNames.includes(variableName)) {
        try {
          let currentDir = HMFileUtil.pathResolve(this.sourceFilePath, HMRouterPluginConstant.PARENT_DELIMITER);
          let tempFilePath = HMFileUtil.pathResolve(currentDir, importPath + HMRouterPluginConstant.ETS_SUFFIX);

          if (HMFileUtil.exist(tempFilePath)) {
            filePath = tempFilePath;
          } else {
            // Try other modules
            try {
              filePath = this.getOtherModuleVariableFilePath(importPath, variableName);
            } catch (error: any) {
              Logger.warn(`Cannot find variable ${variableName} in module ${importPath}: ${error.message}`);
            }
          }
        } catch (error: any) {
          Logger.warn(`Error parsing import path ${importPath}: ${error.message}`);
        }
      }
    }

    // If path not found, provide friendly error message
    if (!filePath) {
      Logger.warn(`Cannot find file path for variable ${variableName}`);
    }

    return filePath;
  }

  /**
   * @description get other module variable file path
   * @param moduleName
   * @param variableName
   * @private
   */
  private getOtherModuleVariableFilePath(moduleName: string, variableName: string): string {
    let moduleFilePath = HMFileUtil.pathResolve(
      this.config.modulePath,
      HMRouterPluginConstant.OH_MODULE_PATH,
      moduleName,
      ConfigConstants.DEFAULT_SCAN_DIR
    );
    if (!HMFileUtil.exist(moduleFilePath)) {
      // Indicates that the module does not exist and needs to search the project directory
      moduleFilePath = HMFileUtil.pathResolve(
        PluginStore.getInstance().projectFilePath,
        HMRouterPluginConstant.OH_MODULE_PATH,
        moduleName,
        ConfigConstants.DEFAULT_SCAN_DIR
      );
    }
    let variableMap;
    if (PluginStore.getInstance().variableCache.has(moduleName)) {
      variableMap = PluginStore.getInstance().variableCache.get(moduleName)!;
    } else {
      variableMap = TsAstUtil.parseCrossModuleVariable(moduleFilePath);
      PluginStore.getInstance().variableCache.set(moduleName, variableMap);
    }
    for (let [key, value] of variableMap) {
      if (value.includes(variableName)) {
        return key;
      }
    }
    throw new Error(`Unknown variable ${variableName} in ${moduleName}`);
  }
}
